# mktest_tb

## Что это
**mktest_tb** создаёт шаблон испытательного стенда с файлами *.sv, *.v, *.sby, *.mcy, *.cpp.

Для работы необходимр иметь какой-нибудь *nix в качестве универсальной среды программирования, ваш любимый текстовый редактор, а также инструменты FOSS/FLOSS [Verilator][2], [iverilog][3], [yosys][4], [gtkwave][5].

Этот проект был создан как ответ на необходимость помнить многочисленные опции, настройки, правила, синтаксис и тому подобное при создании тестовых стендов. Я решил собрать эти вещи в одном проекте, задокументировать их и сделать быстрым и лёгким их использование. Основную ценность здесь представляют Makefile-ы, в которых собраны образцы сборок вариантов использования инструментов. 

Цель **dot** показывает как использовать **yosys** для получения файлов с графическим отображением тестируемого дизайна. 

Цель **exe** собирает стенд с использованием **main.cpp**, хотя ничего и не делает.

Цель **exe_b** собирает стенд с детальным управлением тестируемым модулем под управлением только С++.

Цель **exe_t** аналогична предыдущей цели, но собрана с использованием дополнительного шаблонного класса TESTB, чтобы разгрузить функцию main.

Также, по мере развития проекта, я добавлю образцы скриптов *.sby, *.mcy для выполнения формальной и мутационной проверок.

## 1. Создание проекта 
 
Создать проект можно выполнив команду:
```bash
$ mktest_tb t1
```
либо просто выполните:
```bash
$ mktest_tb
```
программа запросит имя проекта:
```bash
$ Введите имя папки:
```
То, что введено - будет именем директории, в которой будет находится проект.
Будет создана директория со структурой:
```bash
proj
   |_ bench
          |_ cpp
          |_ formal
   |_ rtl
   |_ sim
```
В директории proj как и во всех остальных, будут находится файлы Makefile. В директории rtl будет размещён файл с модулем <projname>.sv. Данный файл содержит простой D-триггер. Это заготовка для проекта и этот файл используется для сборок испытательных стендов. 

Добавил две цели в Makefile:
1. vsim - компилирует тестовый модуль при помощи verilator и запускает gtkwave на просмотр эпюр.
2. isim - делает тоже самое только при помощи iverilog.
Для достижения этих целей надо перейти в директорию проекта
```bash
$ cd prj
```
Затем в корневой директории проекта выполнить
```bash
$ make vsim
```
либо
```bash
$ make isim
```
В итоге gtkwave покажет созданные эпюры сигналов.

## 2. Цель dot
Эта цель настроена на получение файлов *.dot представляющих в графическом виде тестовый модуль. Эта цель строится применением yosys и скрипта, который находится в директории sim/ с именем <имя_проета>.ys. Допустим, что имя проекта t4, тогда скрипт будет выглядеть так: 
```bash
    1  echo on
    2
    3  hierarchy -top t4
    4  select -module t4
    5  select -list
    6  select t:*
    7  select -list
    8  select -set new_cells %
    9  select -clear
   10  show -format dot -prefix t4_show t4
   11  show -format dot -prefix new_cells_show -notitle @new_cells
   12  show -color maroon3 @new_cells -color cornflowerblue p:* -notitle -format dot -prefix t4_hier
   13
   14  # ========================================================
   15
   16  proc -noopt
   17  select -set new_cells t: t:*dff
   18  show -color maroon3 @new_cells -notitle -format dot -prefix t4_proc
   19
   20  # ========================================================
   21
   22  opt_expr; clean
   23  select -set new_cells t:
   24  show -color cornflowerblue @new_cells -notitle -format dot -prefix t4_clean
   25
   26  # ========================================================
```

В этом скрипте записаны правила для получения файлов *.dot. 
- Команда select используется для изменения и просмотра списка выбранных объектов:
- Когда вызывается select -module <module_name> (стр.4) это меняет текущую выборку из всего дизайна только на указанный <module_name> модуль и указывает на то, что любые команды, которые мы запускаем на данном этапе, будут работать только на <module_name>. 
- Вызов select -list (стр.5) даёт список всех объектов в  модуле <module_name>, включая сам модуль, а также все провода, входы, выходы, процессы и ячейки.
- select t:* (стр. 6). Здесь t: означает, что будет происходить отбор с совпадаем по типу ячейки, и * означает сопоставление с чем угодно. Это простое выделение находит все ячейки, независимо от их типа. Активное выделение теперь отображается как [<module_name>]*, указывающее на некоторый подвыбор из модуля <module_name>. Это дает нам ячейки, которые мы хотим выделить для модуля <module_name> после изображения иерархии.

Мы можем присвоить выделенному элементу имя с помощью select -set (8). В нашем случае мы используем название new_cells, и указываем ему использовать текущий выбор, обозначенный символом %. Затем мы можем использовать это именованное выделение, обозначив его как @new_cells, которое мы увидим позже (11, 12). Затем мы очищаем выделение, чтобы следующие команды могли работать с полным дизайном (9). Хотя здесь это разделено, мы могли бы сделать то же самое в одной строке, вызвав select -set new_cells <module_name>/t:*. Если мы знаем, что в нашем проекте есть только один модуль, мы можем пропустить часть <module_name>. Просматривая дальше код,мы можем увидеть это с помощью select -set new_cells t:$mux t:*dff (19). Мы также можем видеть в этой команде, что выбор не обязательно должен ограничиваться одним оператором.

Многие команды также поддерживают необязательный [selection] аргумент, который может быть использован для переопределения выбранных в данный момент объектов. Мы могли бы, например, вызвать clean <module_name> с тем чтобы clean имел возможность оперировать только с <module_name>. 

Команда show создаёт (10, 11, 12, 18, 24) файлы dot с представленным дизайном в соответствии с выбором select.

Подробнее о скриптах yosys см. [YosysHQ. More scripting][1]  

Сборкой занимается Makefile
```bash
.PHONY: dot

dot:\$(TESTNAME).ys
	yosys -s \$^ ../rtl/\$(TESTNAME).sv
```
Здесь yosys вызывается с опцией -s, которой в качестве параметра передаётся имя файла скрипта. Также передаётся имя файла, в котором содержится рассматриваемый модуль.

В корневой директории созданного проекта выполните:
```bash
$ make dot
$ cd sim
```
В директории sim будут созданы файлы с расширением dot. Файлы можно просмотреть в приложении DotViewer или в каком-нибудь другом подобном. Также, если вы перешли в директорию sim, прямо в ней можно выполнить
```bash
$ make dot
```
И вы получите тот же результат.

## 3. Цель exe
Здесь цель состоит в том, чтобы получить исполняемый файл совместно из файлов bench/cpp/sim_main.cpp и rtl/<testname>.sv. Файл sim_main.cpp это предустановленный файл, в котором содержится функция main. Предыдущие цели полагались на то, что verilator сам построит функцию main и соберёт исполняемый файл. Цель exe не полагается на это свойство verilator и требует наличие файла, содержащего функцию main.

Рассмотрим этот файл.
```c++
     1    #include "Vt4.h"
     2    #include "verilated.h"
     3    int main(int argc, char** argv) {
     4        VerilatedContext contextp = new VerilatedContext;
     5        contextp->commandArgs(argc, argv);
     6        Vt4* top = new Vt4{contextp};
     7
     8        while (!contextp->gotFinish()) { top->eval(); }
     9        delete top;
    10        delete contextp;
    11        return 0;
    12    }
```
Он включает сам модуль (1), verilator (2). Далее в main создаётся объект VerilatedContext и указатель contextp на него (4). После чего создаётся экземпляр тестируемого модуля (6) и указатель top на него. Потом в цикле происходит вычисление состояний переменных. Хотя здесь, на самом деле ничего не вычисляется. Просто происходит ожидание нажатия клавиш Ctl+D для остановки цикла (8). Уборка за собой (9, 10) и выход из программы.

Цель собирается при помощи verilator
```bash
exe: del_dir post_exe

del_dir:
	rm -f -r obj_dir

CSRC=../rtl/$(TESTNAME).sv ../bench/cpp/sim_main.cpp 
post_exe: obj_dir/V$(TESTNAME)
obj_dir/V$(TESTNAME): $(CSRC)
	verilator --cc --exe --build -j 0 -Wall $^
```
Здесь опция --cc заставляет verilator получить выходные данные C++ (в отличие, например, от SystemC или только linting).

--exe, вместе с sim_main.cpp файлом-оболочкой, создаст исполняемый файл, а не только библиотеку.

--build заставит Verilator вызвать gcc, чтобы вручную не вызвать его как отдельный шаг.

-j 0 для проверки столько потоков процессора есть на компьютере. 

-Wall Verilator включает более сильные предупреждения.

И в конце - тестируемый модуль <testname>.sv

Для достижения этой цели необходимо выполнить в корневой директории либо в директории sim 
```shell
$ make exe
```
В результате в obj_dir появится исполняемый файл V<testname>. 

На данный момент этот файл ничего не делат. Это будет изменено в дальнейшем.

## 4. Цель exe_b
Здесь я буду созлавать трассировку исключительно на С++. Как и в предыдущем пункте эта цель требует наличия файла, содержащего функцию main. Для этой цели я создал новый файл sim1_main.cpp. 
```c++
     1 #include "Vt4.h"
     2 #include "verilated.h"
     3 #include "verilated_vcd_c.h"
     4
     5 int main(int argc, char** argv) {
     6   Verilated::mkdir("logs");
     7
     8   const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
     9   contextp->debug(0);
    10   contextp->commandArgs(argc, argv);
    11   contextp->randReset(2);
    12   contextp->traceEverOn(true);
    13
    14   VerilatedVcdC* tfp = new VerilatedVcdC;
    15 
    16   const std::unique_ptr<Vt4> top{new Vt4 {contextp.get(), "TOP"}};
    17
    18   top->trace(tfp, 99);  // Trace 99 levels of hierarchy (or see below)
    19   //tfp->dumpvars(1, "t");  // trace 1 level under "t"
    20   tfp->open("out.vcd");
    21
    22   top->rstn = !0; // Установим начальные значения: rstn
    23   top->clk = 0;   // и clk
    24   uint16_t cnt=0; // Вспомогательный счётчик
         // Будем работать до Ctl+D
    25   while (!contextp->gotFinish()) {
    26     contextp->timeInc(1); // 1 timeprecision period passes...
    27     top->clk = !top->clk; // Включам тик на clk
    28     if (!top->clk) {
    29       if (contextp->time() > 1 && contextp->time() < 10) {
    30         top->rstn = !1;  // Assert reset
    31       } else {
    32         top->rstn = !0;  // Deassert reset
    33       }
    34       // Assign some other inputs
    35       // Подаём данные на вход d
    36       if (cnt < 32){
    37         cnt++;
    38       }else{
    39         cnt = 0;
    40         top->d = !top->d;
    41       }
    42     }
    43     top->eval(); // Вычисляем состояние сигналов схемы
    44     tfp->dump(contextp->time()); // Сохраняем время
    45   }
    46   top->final(); // Заканчиваем работу
    47   tfp->close(); // Освобождаем указатель
    ...
    52   // Coverage analysis (calling write only after the test is known to pass)
    53  #if VM_COVERAGE
    54      Verilated::mkdir("logs");
    55      contextp->coveragep()->write("logs/coverage.dat");
    56  #endif
    57
    58  // Final simulation summary
    59  // contextp->statsPrintSummary();
    60  return 0;
    61  }
```
В строке 3 записано требование на включение файла verilated_vcd_c.h. Это необходимо для того, чтобы задействовать объект VerilatedVcdC. В строке 14 создаётся объект VerilatedVcdC и *tfp - указатель на этот объект. 

Далее создаётся экземпляр тестируемой модели и указатель *top - на него см. с.16. 

В с.18 мы подключаем объект трассировки tfp к модулю top и указывваем отслеживать 99 уровней трассироки. Конечно, это избыточно в данном случае.  В с.20 открываем файл out.cvd, в который будут направлены данные трассировки. Дополнительные пояснения смотри в тексте программы.

### Немного о сборке.
Включить трассировку можно двумя способами.
1. Передать опцию --trace для Verilator. После чего можно использовать $dumpfile и $dumpvars как в любом симуляторе Verilog. Хотя Verilator будет игнорировать аргументы $dumpvars. Если не используется флаг --binary, то необходимо вызвать Verilated::traceEverOn(true). Если флаг --binary указан при сборке, Verilator это сделает для вас.

2. Для более детального управления, или для файлов C++ с несколькими верифицированными модулями, можно создать трассировку исключительно на C++.
Для этого необходимо создать объект VerilatedVcdC и в основном цикле, сразу после eval() вызывать trace_object->dump(contextp->time()) на каждом шаге. После завершения основного цикла вызвать trace_object->close(). 

Также необходимо будет скомпилировать verilated_vcd_c.cpp, предпочтительно, добавив зависимости в свой Makefile $(VK_GLOBAL_OBJS). Это делается за вас, если вы используете --binary или --exe вариант.

В моём Makefile это сделано так:
```bash
.PHONY: exe_b

CPPSRC=../rtl/\$(TESTNAME).sv ../bench/cpp/sim1_main.cpp 

exe_b: rm_obj post_exe_b build_exe run_exe_b

rm_obj:
	rm -f -r obj_dir

post_exe_b: \$(CPPSRC)
	verilator --cc --exe --trace --timing -Wall \$^

build_exe:
	make -j -C obj_dir -f V\$(TESTNAME).mk V\$(TESTNAME)

run_exe_b:
	echo "\n\$(TESTNAME) is runnig. To interrupt do Ctl+C\n"
	obj_dir/V\$(TESTNAME)
``` 
В начале вызывается verilator для генерации файлов С++ в директории obj_dir. Потом, для окончательной сборки в пункте build_exe вызывается  Makefile.mk, созданный на предыдущем шаге сборки. В результате будет получен исполняемый файл стенда для симуляции работы модуля.
 
После 
```bash
$ make exe_b
```
получим
```bash
...
t4 is runnig. To interrupt do Ctl+C

obj_dir/Vt4
```
Не забываем остановить программу. Иначе файл out.vcd будет очень большим.
И как обычно
```bash
$ gtkwave out.vcd
```
покажет эпюры на линиях тестируемого модуля.

## 5. Цель exe_t
В  предыдущей цели функция main стала довольно большой для целей тестирования. Кроме того для некоторых тестов появляется необходимость в более тонкой настройке. Чтобы вынести элементы настройки "за кадр" воспользуемся шаблонным классом TESTB [Dan Gisselquist ZipCPU][6]:
<details>
  <summary>testb.h</summary>
<pre>
<code>
     1	#ifndef TESTB_H
     2	#define TESTB_H
     3	
     4	#include <stdio.h>
     5	#include <stdint.h>
     6	#include "verilated.h"
     7	#include <verilated_vcd_c.h>
     8	
     9	#define TBASSERT(TB,A) do { if (!(A)) { (TB).closetrace(); } assert(A); } while(0);
    10	
    11	template <class VA>     class TESTB {
    12	public:
    13	  VA              *m_core;
    14	  VerilatedVcdC*  m_trace;
    15	  uint64_t        m_tickcount;
    16	
    17	  TESTB(void) : m_trace(NULL), m_tickcount(0l) {
    18	    m_core = new VA;
    19	    m_core->clk = 0;
    20	    m_core->rstn = !0;
    21	    eval(); // Get our initial values set properly.
    22	  }
    23	
    24	  virtual ~TESTB(void) {
    25	    closetrace();
    26	    delete m_core;
    27	    m_core = NULL;
    28	  }
    29	         
    30	  virtual void opentrace(const char *vcdname) {
    31	    if (!m_trace) {
    32	      m_trace = new VerilatedVcdC;
    33	      m_core->trace(m_trace, 99);
    34	      m_trace->open(vcdname);
    35	    }
    36	  }
    37	                
    38	  virtual void closetrace(void) {
    39	    if (m_trace) {
    40	      m_trace->close();
    41	      delete m_trace;
    42	      m_trace = NULL;
    43	    }
    44	  }
    45	  virtual void eval(void) {
    46	    m_core->eval();
    47	  }
    48	
    49	  virtual void tick(void) {
    50	    m_tickcount++;
    51	
    52	  // Убедитесь, что мы получили наши оценки непосредственно перед
    53	  // началом работы. Это необходимо, так как в некоторых модулях
    54	  // подключения могли быть внесены изменения, от которых зависит
    55	  // некоторая логика. Это заставляет эту логику пересчитываться до 
          // начала тактового сигнала.
    56	  // Предполагается, что в тестируемом модуле есть сигнал clk.
    57	  // Линия с сигналом d является информационным входом в модуль.
    58	   eval();
    59	  // tick масштабируем на 10
    60	   if (m_trace) m_trace->dump((vluint64_t)(10*m_tickcount-2));
    61	     m_core->clk = 1; // Управляем линией clk
    62	     eval();
    63	     if (m_trace) m_trace->dump((vluint64_t)(10*m_tickcount));
    64	     m_core->clk = 0; // Управляем линией clk
    65	     eval();
    66	     if (m_trace) {
    67	       m_trace->dump((vluint64_t)(10*m_tickcount+5));
    68	       m_trace->flush();
    69	     }
    70	     if((10*m_tickcount)>10 && (10*m_tickcount)< 100){
    71	       m_core->rstn = !1; // Assert reset
    72	     }else {
    73	       m_core->rstn = !0; // Deassert reset
    74	     }
    75	     // Управляем входом d
    76	     if( (10*m_tickcount)%13==0){m_core->d = !m_core->d;}
    77	  }
    78	                
    79	  unsigned long   tickcount(void) {
    80	    return m_tickcount;
    81	  }
    82	};
    83	        
    84	#endif
</code>
</pre>
</details>
Этот шаблон был немного изменён по сравнению с оригиналом. 
В строках (17...22) описан конструктор класса. В строке 18 создаётся экземпляр заданного типа. В строках 19, 20 устанавливаются начальные значения clk, rstn в тестируемом модуле. В строке 21 сразу же вычисляется начальное состояние модуля.

Функция opentrace (30) принимает имя файла, в который будут писаться данные трассировки, создаёт объект трассировки (32), включает трассировку (33). Функция closetrace (38) закрывает все объекты трассировки.

Функция eval() (45...47) - это просто обёртка над одноимённой функцией. 

И самая большая функция tick() (49...82). Сразу увеличиваем счетчмк (50) и вызываем eval() (58), смотри комментарий (52...55). Управляем сигналом clk (61, 64) в зависимости от опорного tickcount. При каждом изменении clk вызываем eval (62, 65), чтобы учесть все изменения в модуле.
Часть (66...69) отвечает за выход из функции. В начале работы модуля устанавливаем rstn (71) на 100 опорных счетов. В строке 76 управляем входом d, просто меняя его значение на противоположное.

Функция tickcount (79...81) возвращает значение m_tickcount.

*Намеренно использую описание в терминах функций, но не в терминах методов.*

Теперь функция main (находится в файле simt_main.cpp) будет выглядеть так.
```c++
     1	  #include "Vt4.h"
     2	  #include "verilated.h"
     3	  #include "testb.h"
     4	
     5	  int main(int argc, char** argv) {
     6	    VerilatedContext *contextp = new VerilatedContext;
     7	    contextp->commandArgs(argc, argv);
     8	    contextp->debug(0);
     9	    contextp->randReset(2);
    10	    contextp->traceEverOn(true);
    11	
    12	    TESTB<Vt4> *tb = new TESTB<Vt4>;
    13	
    14	    tb->opentrace("out.vcd");
    15	
    16	    while (!contextp->gotFinish()) { tb->tick(); }
    17	
    18	    tb->closetrace();
    19	    delete tb;
    20	    delete contextp;
    21	
    22	    return 0;
    23	  }
```
Открываем объект VerilatedContext и создаём указатель на него contextp (6). Включаем уровень отладки 0 (8). Настраиваем генератор случайных чисел (9). И включаем трассировку событий (10). Создаём экземпляр тестируемого модуля (12). Открываем файл для записи результатов трассировки (14). Запускаем главный цикл стенда (16). Здесь главное не забывать прервать работу программы, иначе очень быстро закончится место на жестком диске. После того как гдавный цикл будет прерван, удаляем созданные объекты (18...20). И выходим из стенда с кодом 0 (22).

И, как обычно, запускаем сборку стенда и сразу включаем стенд в работу. Следите за этим. 

> Во время остановите стенд Ctl+Z.

Процесс сборки этой цели аналогичен процессу сборки цели exe_b. Поэтому останавливаться на нём не буду. 
```bash
$ make exe_t

...
t4 is runnig. To interrupt do Ctl+C

obj_dir/Vt4
^Z
[42]+  Stopped                 make exe_t
```
Цель будет собрана и исполнена. 
```bash
$ gtkwave out.vcd
```
покажет эпюры напряжений на сигнальных линиях вашего дизайна.
## Обращение
Работа над этим проектом будет продолжаться и далее. Вносите свой вклад энергии необходимой для дальнейшей работы. Используйте этот проект так как Вам удобно.

Приветствуются так же конструктивные замечания и предложения. Буду рад обсудить их и, возможно, внести в проект.

В дальнейшем будут добавлены новые цели.

Я в тлг: @SergeBN
https://t.me/SergeBN

<!--Reference links in article-->
[1]: https://yosyshq.readthedocs.io/projects/yosys/en/latest/using_yosys/more_scripting/index.html
[2]: https://github.com/verilator/verilator
[3]: https://github.com/steveicarus/iverilog
[4]: https://github.com/YosysHQ/yosys
[5]: https://github.com/gtkwave/gtkwave
[6]: https://github.com/ZipCPU